/*
 *
 *  *  Copyright 2014 Orient Technologies LTD (info(at)orientechnologies.com)
 *  *
 *  *  Licensed under the Apache License, Version 2.0 (the "License");
 *  *  you may not use this file except in compliance with the License.
 *  *  You may obtain a copy of the License at
 *  *
 *  *       http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  *  Unless required by applicable law or agreed to in writing, software
 *  *  distributed under the License is distributed on an "AS IS" BASIS,
 *  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  *  See the License for the specific language governing permissions and
 *  *  limitations under the License.
 *  *
 *  * For more information: http://www.orientechnologies.com
 *
 */
package com.orientechnologies.orient.core.fetch;

import com.orientechnologies.common.collection.OMultiCollectionIterator;
import com.orientechnologies.common.collection.OMultiValue;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.db.record.ORecordLazyMultiValue;
import com.orientechnologies.orient.core.db.record.ridbag.ORidBag;
import com.orientechnologies.orient.core.exception.ORecordNotFoundException;
import com.orientechnologies.orient.core.fetch.json.OJSONFetchContext;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.serialization.serializer.OStringSerializerHelper;

import java.io.IOException;
import java.lang.reflect.Array;
import java.util.*;

/**
 * Helper class for fetching.
 *
 * @author Luca Garulli (l.garulli--at--orientechnologies.com)
 * @author Luca Molino
 * @author Claudio Tesoriero (giastfader @ github)
 */
public class OFetchHelper {
  public static final String     DEFAULT           = "*:0";
  public static final OFetchPlan DEFAULT_FETCHPLAN = new OFetchPlan(DEFAULT);

  public static OFetchPlan buildFetchPlan(final String iFetchPlan) {
    if (iFetchPlan == null)
      return null;

    if (DEFAULT.equals(iFetchPlan))
      return DEFAULT_FETCHPLAN;

    return new OFetchPlan(iFetchPlan);
  }

  public static void fetch(final ORecord iRootRecord, final Object iUserObject, final OFetchPlan iFetchPlan,
      final OFetchListener iListener, final OFetchContext iContext, final String iFormat) {
    try {
      if (iRootRecord instanceof ODocument) {
        // SCHEMA AWARE
        final ODocument record = (ODocument) iRootRecord;
        final Map<ORID, Integer> parsedRecords = new HashMap<ORID, Integer>();

        final boolean isEmbedded = record.isEmbedded() || !record.getIdentity().isPersistent();
        if (!isEmbedded)
          parsedRecords.put(iRootRecord.getIdentity(), 0);

        if (!iFormat.contains("shallow"))
          processRecordRidMap(record, iFetchPlan, 0, 0, -1, parsedRecords, "", iContext);

        processRecord(record, iUserObject, iFetchPlan, 0, 0, -1, parsedRecords, "", iListener, iContext, iFormat);
      }
    } catch (Exception e) {
      OLogManager.instance().error(null, "Fetching error on record %s", e, iRootRecord.getIdentity());
    }
  }

  public static void checkFetchPlanValid(final String iFetchPlan) {

    if (iFetchPlan != null && !iFetchPlan.isEmpty()) {
      // CHECK IF THERE IS SOME FETCH-DEPTH
      final List<String> planParts = OStringSerializerHelper.split(iFetchPlan, ' ');
      if (!planParts.isEmpty()) {
        for (String planPart : planParts) {
          final List<String> parts = OStringSerializerHelper.split(planPart, ':');
          if (parts.size() != 2) {
            throw new IllegalArgumentException("Fetch plan '" + iFetchPlan + "' is invalid");
          }
        }
      } else {
        throw new IllegalArgumentException("Fetch plan '" + iFetchPlan + "' is invalid");
      }
    }

  }

  public static boolean isFetchPlanValid(final String iFetchPlan) {

    if (iFetchPlan != null && !iFetchPlan.isEmpty()) {
      // CHECK IF THERE IS SOME FETCH-DEPTH
      final List<String> planParts = OStringSerializerHelper.split(iFetchPlan, ' ');
      if (!planParts.isEmpty()) {
        for (String planPart : planParts) {
          final List<String> parts = OStringSerializerHelper.split(planPart, ':');
          if (parts.size() != 2) {
            return false;
          }
        }
      } else {
        return false;
      }
    }

    return true;

  }

  private static int getDepthLevel(final OFetchPlan iFetchPlan, final String iFieldPath, final int iCurrentLevel) {
    if (iFetchPlan == null)
      return 0;
    return iFetchPlan.getDepthLevel(iFieldPath, iCurrentLevel);
  }

  public static void processRecordRidMap(final ODocument record, final OFetchPlan iFetchPlan, final int iCurrentLevel,
      final int iLevelFromRoot, final int iFieldDepthLevel, final Map<ORID, Integer> parsedRecords, final String iFieldPathFromRoot,
      final OFetchContext iContext) throws IOException {
    if (iFetchPlan == null)
      return;

    if (iFetchPlan == OFetchHelper.DEFAULT_FETCHPLAN)
      return;

    Object fieldValue;
    for (String fieldName : record.fieldNames()) {
      int depthLevel;
      final String fieldPath = !iFieldPathFromRoot.isEmpty() ? iFieldPathFromRoot + "." + fieldName : fieldName;

      depthLevel = getDepthLevel(iFetchPlan, fieldPath, iCurrentLevel);
      if (depthLevel == -2)
        continue;
      if (iFieldDepthLevel > -1)
        depthLevel = iFieldDepthLevel;

      fieldValue = record.rawField(fieldName);
      if (fieldValue == null || !(fieldValue instanceof OIdentifiable)
          && (!(fieldValue instanceof ORecordLazyMultiValue) || !((ORecordLazyMultiValue) fieldValue).rawIterator().hasNext()
              || !(((ORecordLazyMultiValue) fieldValue).rawIterator().next() instanceof OIdentifiable))
          && (!(fieldValue instanceof Collection<?>) || ((Collection<?>) fieldValue).size() == 0
              || !(((Collection<?>) fieldValue).iterator().next() instanceof OIdentifiable))
          && (!(fieldValue.getClass().isArray()) || Array.getLength(fieldValue) == 0
              || !(Array.get(fieldValue, 0) instanceof OIdentifiable))
          && (!(fieldValue instanceof OMultiCollectionIterator<?>))
          && (!(fieldValue instanceof Map<?, ?>) || ((Map<?, ?>) fieldValue).size() == 0
              || !(((Map<?, ?>) fieldValue).values().iterator().next() instanceof OIdentifiable))) {
        continue;
      } else {
        try {
          final boolean isEmbedded = isEmbedded(fieldValue);
          if (iFetchPlan == null || (!(isEmbedded && iContext.fetchEmbeddedDocuments()) && !iFetchPlan.has(fieldPath, iCurrentLevel)
              && depthLevel > -1 && iCurrentLevel >= depthLevel))
            // MAX DEPTH REACHED: STOP TO FETCH THIS FIELD
            continue;

          final int nextLevel = isEmbedded ? iLevelFromRoot : iLevelFromRoot + 1;

          if (fieldValue instanceof ORecordId)
            fieldValue = ((ORecordId) fieldValue).getRecord();

          fetchRidMap(record, iFetchPlan, fieldValue, fieldName, iCurrentLevel, nextLevel, iFieldDepthLevel, parsedRecords,
              fieldPath, iContext);
        } catch (Exception e) {
          OLogManager.instance().error(null, "Fetching error on record %s", e, record.getIdentity());
        }
      }
    }
  }

  private static void fetchRidMap(final ODocument iRootRecord, final OFetchPlan iFetchPlan, final Object fieldValue,
      final String fieldName, final int iCurrentLevel, final int iLevelFromRoot, final int iFieldDepthLevel,
      final Map<ORID, Integer> parsedRecords, final String iFieldPathFromRoot, final OFetchContext iContext) throws IOException {
    if (fieldValue == null) {
      return;
    } else if (fieldValue instanceof ODocument) {
      fetchDocumentRidMap(iFetchPlan, fieldValue, fieldName, iCurrentLevel, iLevelFromRoot, iFieldDepthLevel, parsedRecords,
          iFieldPathFromRoot, iContext);
    } else if (fieldValue instanceof Iterable<?>) {
      fetchCollectionRidMap(iFetchPlan, fieldValue, fieldName, iCurrentLevel, iLevelFromRoot, iFieldDepthLevel, parsedRecords,
          iFieldPathFromRoot, iContext);
    } else if (fieldValue.getClass().isArray()) {
      fetchArrayRidMap(iFetchPlan, fieldValue, fieldName, iCurrentLevel, iLevelFromRoot, iFieldDepthLevel, parsedRecords,
          iFieldPathFromRoot, iContext);
    } else if (fieldValue instanceof Map<?, ?>) {
      fetchMapRidMap(iFetchPlan, fieldValue, fieldName, iCurrentLevel, iLevelFromRoot, iFieldDepthLevel, parsedRecords,
          iFieldPathFromRoot, iContext);
    }
  }

  private static void fetchDocumentRidMap(final OFetchPlan iFetchPlan, Object fieldValue, String fieldName, final int iCurrentLevel,
      final int iLevelFromRoot, final int iFieldDepthLevel, final Map<ORID, Integer> parsedRecords, final String iFieldPathFromRoot,
      final OFetchContext iContext) throws IOException {
    updateRidMap(iFetchPlan, (ODocument) fieldValue, iCurrentLevel, iLevelFromRoot, iFieldDepthLevel, parsedRecords,
        iFieldPathFromRoot, iContext);
  }

  @SuppressWarnings("unchecked")
  private static void fetchCollectionRidMap(final OFetchPlan iFetchPlan, final Object fieldValue, final String fieldName,
      final int iCurrentLevel, final int iLevelFromRoot, final int iFieldDepthLevel, final Map<ORID, Integer> parsedRecords,
      final String iFieldPathFromRoot, final OFetchContext iContext) throws IOException {
    final Iterable<OIdentifiable> linked = (Iterable<OIdentifiable>) fieldValue;
    for (OIdentifiable d : linked) {
      if (d != null) {
        // GO RECURSIVELY
        d = d.getRecord();

        updateRidMap(iFetchPlan, (ODocument) d, iCurrentLevel, iLevelFromRoot, iFieldDepthLevel, parsedRecords, iFieldPathFromRoot,
            iContext);
      }
    }
  }

  private static void fetchArrayRidMap(final OFetchPlan iFetchPlan, final Object fieldValue, final String fieldName,
      final int iCurrentLevel, final int iLevelFromRoot, final int iFieldDepthLevel, final Map<ORID, Integer> parsedRecords,
      final String iFieldPathFromRoot, final OFetchContext iContext) throws IOException {
    if (fieldValue instanceof ODocument[]) {
      final ODocument[] linked = (ODocument[]) fieldValue;
      for (ODocument d : linked)
        // GO RECURSIVELY
        updateRidMap(iFetchPlan, (ODocument) d, iCurrentLevel, iLevelFromRoot, iFieldDepthLevel, parsedRecords, iFieldPathFromRoot,
            iContext);
    }
  }

  @SuppressWarnings("unchecked")
  private static void fetchMapRidMap(final OFetchPlan iFetchPlan, Object fieldValue, String fieldName, final int iCurrentLevel,
      final int iLevelFromRoot, final int iFieldDepthLevel, final Map<ORID, Integer> parsedRecords, final String iFieldPathFromRoot,
      final OFetchContext iContext) throws IOException {
    final Map<String, ODocument> linked = (Map<String, ODocument>) fieldValue;
    for (ODocument d : (linked).values())
      // GO RECURSIVELY
      updateRidMap(iFetchPlan, (ODocument) d, iCurrentLevel, iLevelFromRoot, iFieldDepthLevel, parsedRecords, iFieldPathFromRoot,
          iContext);
  }

  private static void updateRidMap(final OFetchPlan iFetchPlan, final ODocument fieldValue, final int iCurrentLevel,
      final int iLevelFromRoot, final int iFieldDepthLevel, final Map<ORID, Integer> parsedRecords, final String iFieldPathFromRoot,
      final OFetchContext iContext) throws IOException {
    if (fieldValue == null)
      return;

    final Integer fetchedLevel = parsedRecords.get(fieldValue.getIdentity());
    int currentLevel = iCurrentLevel + 1;
    int fieldDepthLevel = iFieldDepthLevel;
    if (iFetchPlan != null && iFetchPlan.has(iFieldPathFromRoot, iCurrentLevel)) {
      currentLevel = 1;
      fieldDepthLevel = iFetchPlan.getDepthLevel(iFieldPathFromRoot, iCurrentLevel);
    }

    final boolean isEmbedded = isEmbedded(fieldValue);

    if (isEmbedded || fetchedLevel == null) {
      if (!isEmbedded)
        parsedRecords.put(fieldValue.getIdentity(), iLevelFromRoot);

      processRecordRidMap(fieldValue, iFetchPlan, currentLevel, iLevelFromRoot, fieldDepthLevel, parsedRecords, iFieldPathFromRoot,
          iContext);
    }
  }

  private static void processRecord(final ODocument record, final Object iUserObject, final OFetchPlan iFetchPlan,
      final int iCurrentLevel, final int iLevelFromRoot, final int iFieldDepthLevel, final Map<ORID, Integer> parsedRecords,
      final String iFieldPathFromRoot, final OFetchListener iListener, final OFetchContext iContext, final String iFormat)
      throws IOException {

    if (record == null)
      return;

    if (!iListener.requireFieldProcessing() && iFetchPlan == OFetchHelper.DEFAULT_FETCHPLAN)
      return;

    Object fieldValue;

    iContext.onBeforeFetch(record);
    Set<String> toRemove = new HashSet<String>();

    for (String fieldName : record.fieldNames()) {
      String fieldPath = !iFieldPathFromRoot.isEmpty() ? iFieldPathFromRoot + "." + fieldName : fieldName;
      int depthLevel;
      depthLevel = getDepthLevel(iFetchPlan, fieldPath, iCurrentLevel);
      if (depthLevel == -2) {
        toRemove.add(fieldName);
        continue;
      }
      if (iFieldDepthLevel > -1)
        depthLevel = iFieldDepthLevel;

      fieldValue = record.rawField(fieldName);
      OType fieldType = record.fieldType(fieldName);

      boolean fetch = !iFormat.contains("shallow") && (!(fieldValue instanceof OIdentifiable) || depthLevel == -1
          || iCurrentLevel <= depthLevel || (iFetchPlan != null && iFetchPlan.has(fieldPath, iCurrentLevel)));

      final boolean isEmbedded = isEmbedded(fieldValue);

      if (!fetch && isEmbedded && iContext.fetchEmbeddedDocuments())
        // EMBEDDED, GO DEEPER
        fetch = true;

      if (iFormat.contains("shallow") || fieldValue == null || (!fetch && fieldValue instanceof OIdentifiable)
          || !(fieldValue instanceof OIdentifiable)
              && (!(fieldValue instanceof ORecordLazyMultiValue) || !((ORecordLazyMultiValue) fieldValue).rawIterator().hasNext()
                  || !(((ORecordLazyMultiValue) fieldValue).rawIterator().next() instanceof OIdentifiable))
              && (!(fieldValue.getClass().isArray()) || Array.getLength(fieldValue) == 0
                  || !(Array.get(fieldValue, 0) instanceof OIdentifiable))
              && !containsIdentifiers(fieldValue)) {
        iContext.onBeforeStandardField(fieldValue, fieldName, iUserObject, fieldType);
        iListener.processStandardField(record, fieldValue, fieldName, iContext, iUserObject, iFormat, fieldType);
        iContext.onAfterStandardField(fieldValue, fieldName, iUserObject, fieldType);
      } else {
        try {
          if (fetch) {
            final int nextLevel = isEmbedded ? iLevelFromRoot : iLevelFromRoot + 1;

            fetch(record, iUserObject, iFetchPlan, fieldValue, fieldName, iCurrentLevel, nextLevel, iFieldDepthLevel, parsedRecords,
                depthLevel, fieldPath, iListener, iContext);
          }

        } catch (Exception e) {
          OLogManager.instance().error(null, "Fetching error on record %s", e, record.getIdentity());
        }
      }
    }
    for (String fieldName : toRemove) {
      iListener.skipStandardField(record, fieldName, iContext, iUserObject, iFormat);
    }
    iContext.onAfterFetch(record);
  }

  private static boolean containsIdentifiers(Object fieldValue) {
    if (!OMultiValue.isMultiValue(fieldValue)) {
      return false;
    }
    for (Object item : OMultiValue.getMultiValueIterable(fieldValue)) {
      if (item instanceof OIdentifiable) {
        return true;
      }
      if (containsIdentifiers(item)) {
        return true;
      }
    }
    return false;
  }

  public static boolean isEmbedded(Object fieldValue) {
    boolean isEmbedded = fieldValue instanceof ODocument
        && (((ODocument) fieldValue).isEmbedded() || !((ODocument) fieldValue).getIdentity().isPersistent());

    // ridbag can contain only edges no embedded documents are allowed.
    if (fieldValue instanceof ORidBag)
      return false;
    if (!isEmbedded) {
      try {
        final Object f = OMultiValue.getFirstValue(fieldValue);
        isEmbedded = f != null
            && (f instanceof ODocument && (((ODocument) f).isEmbedded() || !((ODocument) f).getIdentity().isPersistent()));
      } catch (Exception e) {
        // IGNORE IT
      }
    }
    return isEmbedded;
  }

  private static void fetch(final ODocument iRootRecord, final Object iUserObject, final OFetchPlan iFetchPlan,
      final Object fieldValue, final String fieldName, final int iCurrentLevel, final int iLevelFromRoot,
      final int iFieldDepthLevel, final Map<ORID, Integer> parsedRecords, final int depthLevel, final String iFieldPathFromRoot,
      final OFetchListener iListener, final OFetchContext iContext) throws IOException {

    int currentLevel = iCurrentLevel + 1;
    int fieldDepthLevel = iFieldDepthLevel;
    if (iFetchPlan != null && iFetchPlan.has(iFieldPathFromRoot, iCurrentLevel)) {
      currentLevel = 0;
      fieldDepthLevel = iFetchPlan.getDepthLevel(iFieldPathFromRoot, iCurrentLevel);
    }

    if (fieldValue == null) {
      iListener.processStandardField(iRootRecord, null, fieldName, iContext, iUserObject, "", null);
    } else if (fieldValue instanceof OIdentifiable) {
      fetchDocument(iRootRecord, iUserObject, iFetchPlan, (OIdentifiable) fieldValue, fieldName, currentLevel, iLevelFromRoot,
          fieldDepthLevel, parsedRecords, iFieldPathFromRoot, iListener, iContext);

    } else if (fieldValue instanceof Map<?, ?>) {
      fetchMap(iRootRecord, iUserObject, iFetchPlan, fieldValue, fieldName, currentLevel, iLevelFromRoot, fieldDepthLevel,
          parsedRecords, iFieldPathFromRoot, iListener, iContext);
    } else if (OMultiValue.isMultiValue(fieldValue)) {
      fetchCollection(iRootRecord, iUserObject, iFetchPlan, fieldValue, fieldName, currentLevel, iLevelFromRoot, fieldDepthLevel,
          parsedRecords, iFieldPathFromRoot, iListener, iContext);
    } else if (fieldValue.getClass().isArray()) {
      fetchArray(iRootRecord, iUserObject, iFetchPlan, fieldValue, fieldName, currentLevel, iLevelFromRoot, fieldDepthLevel,
          parsedRecords, iFieldPathFromRoot, iListener, iContext);
    }
  }

  @SuppressWarnings("unchecked")
  private static void fetchMap(final ODocument iRootRecord, final Object iUserObject, final OFetchPlan iFetchPlan,
      Object fieldValue, String fieldName, final int iCurrentLevel, final int iLevelFromRoot, final int iFieldDepthLevel,
      final Map<ORID, Integer> parsedRecords, final String iFieldPathFromRoot, final OFetchListener iListener,
      final OFetchContext iContext) throws IOException {
    final Map<String, ODocument> linked = (Map<String, ODocument>) fieldValue;
    iContext.onBeforeMap(iRootRecord, fieldName, iUserObject);

    for (Object key : linked.keySet()) {
      final Object o = linked.get(key);

      if (o instanceof OIdentifiable) {
        ORecord r = null;
        try {
          r = ((OIdentifiable) o).getRecord();
        } catch (ORecordNotFoundException notFound) {
        }
        if (r != null) {
          if (r instanceof ODocument) {
            // GO RECURSIVELY
            final ODocument d = (ODocument) r;
            final Integer fieldDepthLevel = parsedRecords.get(d.getIdentity());
            if (!d.getIdentity().isValid() || (fieldDepthLevel != null && fieldDepthLevel.intValue() == iLevelFromRoot)) {
              removeParsedFromMap(parsedRecords, d);
              iContext.onBeforeDocument(iRootRecord, d, key.toString(), iUserObject);
              final Object userObject = iListener.fetchLinkedMapEntry(iRootRecord, iUserObject, fieldName, key.toString(), d,
                  iContext);
              processRecord(d, userObject, iFetchPlan, iCurrentLevel, iLevelFromRoot, iFieldDepthLevel, parsedRecords,
                  iFieldPathFromRoot, iListener, iContext, "");
              iContext.onAfterDocument(iRootRecord, d, key.toString(), iUserObject);
            } else {
              iListener.parseLinked(iRootRecord, d, iUserObject, key.toString(), iContext);
            }
          } else
            iListener.parseLinked(iRootRecord, r, iUserObject, key.toString(), iContext);

        }else {
          iListener.processStandardField(iRootRecord, o, key.toString(), iContext, iUserObject, "", null);
        }
      } else if (o instanceof Map) {
        fetchMap(iRootRecord, iUserObject, iFetchPlan, o, key.toString(), iCurrentLevel + 1, iLevelFromRoot, iFieldDepthLevel,
            parsedRecords, iFieldPathFromRoot, iListener, iContext);
      } else if (OMultiValue.isMultiValue(o)) {
        fetchCollection(iRootRecord, iUserObject, iFetchPlan, o, key.toString(), iCurrentLevel + 1, iLevelFromRoot,
            iFieldDepthLevel, parsedRecords, iFieldPathFromRoot, iListener, iContext);
      } else
        iListener.processStandardField(iRootRecord, o, key.toString(), iContext, iUserObject, "", null);
    }
    iContext.onAfterMap(iRootRecord, fieldName, iUserObject);
  }

  private static void fetchArray(final ODocument iRootRecord, final Object iUserObject, final OFetchPlan iFetchPlan,
      Object fieldValue, String fieldName, final int iCurrentLevel, final int iLevelFromRoot, final int iFieldDepthLevel,
      final Map<ORID, Integer> parsedRecords, final String iFieldPathFromRoot, final OFetchListener iListener,
      final OFetchContext iContext) throws IOException {
    if (fieldValue instanceof ODocument[]) {
      final ODocument[] linked = (ODocument[]) fieldValue;
      iContext.onBeforeArray(iRootRecord, fieldName, iUserObject, linked);
      for (ODocument d : linked) {
        // GO RECURSIVELY
        final Integer fieldDepthLevel = parsedRecords.get(d.getIdentity());
        if (!d.getIdentity().isValid() || (fieldDepthLevel != null && fieldDepthLevel.intValue() == iLevelFromRoot)) {
          removeParsedFromMap(parsedRecords, d);
          iContext.onBeforeDocument(iRootRecord, d, fieldName, iUserObject);
          final Object userObject = iListener.fetchLinked(iRootRecord, iUserObject, fieldName, d, iContext);
          processRecord(d, userObject, iFetchPlan, iCurrentLevel, iLevelFromRoot, iFieldDepthLevel, parsedRecords,
              iFieldPathFromRoot, iListener, iContext, "");
          iContext.onAfterDocument(iRootRecord, d, fieldName, iUserObject);
        } else {
          iListener.parseLinkedCollectionValue(iRootRecord, d, iUserObject, fieldName, iContext);
        }
      }
      iContext.onAfterArray(iRootRecord, fieldName, iUserObject);
    } else {
      iListener.processStandardField(iRootRecord, fieldValue, fieldName, iContext, iUserObject, "", null);
    }
  }

  @SuppressWarnings("unchecked")
  private static void fetchCollection(final ODocument iRootRecord, final Object iUserObject, final OFetchPlan iFetchPlan,
      final Object fieldValue, final String fieldName, final int iCurrentLevel, final int iLevelFromRoot,
      final int iFieldDepthLevel, final Map<ORID, Integer> parsedRecords, final String iFieldPathFromRoot,
      final OFetchListener iListener, final OFetchContext iContext) throws IOException {
    final Iterable<?> linked;
    if (fieldValue instanceof Iterable<?> || fieldValue instanceof ORidBag) {
      linked = (Iterable<OIdentifiable>) fieldValue;
      iContext.onBeforeCollection(iRootRecord, fieldName, iUserObject, (Iterable) linked);
    } else if (fieldValue.getClass().isArray()) {
      linked = OMultiValue.getMultiValueIterable(fieldValue, false);
      iContext.onBeforeCollection(iRootRecord, fieldName, iUserObject, (Iterable) linked);
    } else if (fieldValue instanceof Map<?, ?>) {
      linked = (Collection<?>) ((Map<?, ?>) fieldValue).values();
      iContext.onBeforeMap(iRootRecord, fieldName, iUserObject);
    } else
      throw new IllegalStateException("Unrecognized type: " + fieldValue.getClass());

    final Iterator<?> iter;
    if (linked instanceof ORecordLazyMultiValue)
      iter = ((ORecordLazyMultiValue) linked).rawIterator();
    else
      iter = linked.iterator();

    try {
      while (iter.hasNext()) {
        final Object o = iter.next();
        if (o == null)
          continue;

        if (o instanceof OIdentifiable) {
          OIdentifiable d = (OIdentifiable) o;

          // GO RECURSIVELY
          final Integer fieldDepthLevel = parsedRecords.get(d.getIdentity());
          if (!d.getIdentity().isPersistent() || (fieldDepthLevel != null && fieldDepthLevel.intValue() == iLevelFromRoot)) {
            removeParsedFromMap(parsedRecords, d);
            d = d.getRecord();

            if (d == null)
              iListener.processStandardField(null, d, null, iContext, iUserObject, "", null);
            else if (!(d instanceof ODocument)) {
              iListener.processStandardField(null, d, fieldName, iContext, iUserObject, "", null);
            } else {
              iContext.onBeforeDocument(iRootRecord, (ODocument) d, fieldName, iUserObject);
              final Object userObject = iListener.fetchLinkedCollectionValue(iRootRecord, iUserObject, fieldName, (ODocument) d,
                  iContext);
              processRecord((ODocument) d, userObject, iFetchPlan, iCurrentLevel, iLevelFromRoot, iFieldDepthLevel, parsedRecords,
                  iFieldPathFromRoot, iListener, iContext, "");
              iContext.onAfterDocument(iRootRecord, (ODocument) d, fieldName, iUserObject);
            }
          } else {
            iListener.parseLinkedCollectionValue(iRootRecord, d, iUserObject, fieldName, iContext);
          }
        } else if (o instanceof Map<?, ?>) {
          fetchMap(iRootRecord, iUserObject, iFetchPlan, o, null, iCurrentLevel + 1, iLevelFromRoot, iFieldDepthLevel,
              parsedRecords, iFieldPathFromRoot, iListener, iContext);
        } else if (OMultiValue.isMultiValue(o)) {
          fetchCollection(iRootRecord, iUserObject, iFetchPlan, o, null, iCurrentLevel + 1, iLevelFromRoot, iFieldDepthLevel,
              parsedRecords, iFieldPathFromRoot, iListener, iContext);
        } else if (o instanceof String || o instanceof Number || o instanceof Boolean) {
          ((OJSONFetchContext) iContext).getJsonWriter().writeValue(0, false, o);
        }
      }
    } finally {
      if (fieldValue instanceof Iterable<?> || fieldValue instanceof ORidBag)
        iContext.onAfterCollection(iRootRecord, fieldName, iUserObject);
      else if (fieldValue.getClass().isArray())
        iContext.onAfterCollection(iRootRecord, fieldName, iUserObject);
      else if (fieldValue instanceof Map<?, ?>)
        iContext.onAfterMap(iRootRecord, fieldName, iUserObject);
    }
  }

  private static void fetchDocument(final ODocument iRootRecord, final Object iUserObject, final OFetchPlan iFetchPlan,
      final OIdentifiable fieldValue, final String fieldName, final int iCurrentLevel, final int iLevelFromRoot,
      final int iFieldDepthLevel, final Map<ORID, Integer> parsedRecords, final String iFieldPathFromRoot,
      final OFetchListener iListener, final OFetchContext iContext) throws IOException {
    if (fieldValue instanceof ORID && !((ORID) fieldValue).isValid()) {
      // RID NULL: TREAT AS "NULL" VALUE
      iContext.onBeforeStandardField(fieldValue, fieldName, iRootRecord, null);
      iListener.parseLinked(iRootRecord, fieldValue, iUserObject, fieldName, iContext);
      iContext.onAfterStandardField(fieldValue, fieldName, iRootRecord, null);
      return;
    }

    final Integer fieldDepthLevel = parsedRecords.get(fieldValue.getIdentity());
    if (!fieldValue.getIdentity().isValid() || (fieldDepthLevel != null && fieldDepthLevel.intValue() == iLevelFromRoot)) {
      removeParsedFromMap(parsedRecords, fieldValue);
      final ODocument linked = (ODocument) fieldValue.getRecord();
      if (linked == null)
        return;

      iContext.onBeforeDocument(iRootRecord, linked, fieldName, iUserObject);
      Object userObject = iListener.fetchLinked(iRootRecord, iUserObject, fieldName, linked, iContext);
      processRecord(linked, userObject, iFetchPlan, iCurrentLevel, iLevelFromRoot, iFieldDepthLevel, parsedRecords,
          iFieldPathFromRoot, iListener, iContext, "");
      iContext.onAfterDocument(iRootRecord, linked, fieldName, iUserObject);
    } else {
      iContext.onBeforeStandardField(fieldValue, fieldName, iRootRecord, null);
      iListener.parseLinked(iRootRecord, fieldValue, iUserObject, fieldName, iContext);
      iContext.onAfterStandardField(fieldValue, fieldName, iRootRecord, null);
    }
  }

  protected static void removeParsedFromMap(final Map<ORID, Integer> parsedRecords, OIdentifiable d) {
    parsedRecords.remove(d.getIdentity());
  }
}
